﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Workflows.Components.Entities.Relations.InferredAssociations
{
    public class HierarchicalCluster:IClusterCalculator
    {
        #region fields

        private Dictionary<Cluster, Dictionary<Entity, double>> _ClusteredEntities;
        private List<string> _OrphanEntityIDs;
        private List<int> _RootClusterIDs;
        private Dictionary<string, Entity> _Entities;
        private Dictionary<int, Cluster> _Clusters;
        private Dictionary<string, Dictionary<string, double>> _EntityEntityDistances;
        
        #endregion

        #region IClusterCalculator Members

        public void TryCluster(
            EntityMetaData metadata, List<Entity> entities,
            ref Dictionary<Cluster, Dictionary<Entity, double>> clusteredEntities)
        {
            this.InitializeEntities(metadata, entities);

            while(this.CanContinueCluster())
            {
                bool linked = this.Grow(metadata);
                if(!linked)
                    break;
            }

            clusteredEntities = this._ClusteredEntities;
        }

        #endregion

        #region init
        private void InitializeEntities(EntityMetaData metadata, List<Entity> entities)
        {
            this._Entities = new Dictionary<string, Entity>();
            this._Clusters=new Dictionary<int, Cluster>();
            this._ClusteredEntities=new Dictionary<Cluster, Dictionary<Entity, double>>();
            this._OrphanEntityIDs = new List<string>();
            foreach(Entity entity in entities)
            {
                this._OrphanEntityIDs.Add(entity.Id);
                this._Entities.Add(entity.Id,entity);
            }
            this._RootClusterIDs=new List<int>();
            this._EntityEntityDistances = new Dictionary<string, Dictionary<string, double>>();
            
            for(int i=0;i<entities.Count;i++)
            {
                Entity entity1 = entities[i];
                Dictionary<string, double> entityToEntityDistances = new Dictionary<string, double>();
                for(int j=0;j<entities.Count;j++)
                {
                    double entity_entity_distance = 0;
                    Entity entity2 = entities[j];
                    entity_entity_distance = metadata.CompareEntity(entity1, entity2);
                    entityToEntityDistances.Add(entity2.Id,entity_entity_distance);
                }
                this._EntityEntityDistances.Add(entity1.Id,entityToEntityDistances);
            }
        }

        #endregion

        #region iteration decision
        private bool CanContinueCluster()
        {
            if(this._OrphanEntityIDs.Count>0)
                return true;
            if(this._RootClusterIDs.Count>0)
                return true;
            return false;
        }
        #endregion

        #region single link conglomerate
        private bool Grow(EntityMetaData metaData)
        {
            bool linked = this.TryLinkEntityToEntity(metaData);
            if(!linked)
            {
                linked = this.TryLinkEntityToCluster(metaData);
            }
            if(!linked)
            {
                linked = this.TryLinkClusterToCluster(metaData);
            }
            return linked;
        }

        private bool TryLinkEntityToEntity(EntityMetaData metaData)
        {
            double distance = double.PositiveInfinity;
            string[] linkableEntityIDs = new string[2];
            foreach (string entityId1 in this._OrphanEntityIDs)
            {
                foreach (string entityId2 in this._OrphanEntityIDs)
                {
                    if(entityId1!=entityId2)
                    {
                        double entity_entity_distance = this._EntityEntityDistances[entityId1][entityId2];
                        if(entity_entity_distance<distance)
                        {
                            distance = entity_entity_distance;
                            linkableEntityIDs[0] = entityId1;
                            linkableEntityIDs[1] = entityId2;
                        }
                    }
                }
            }

            if (distance < double.PositiveInfinity && linkableEntityIDs != null && linkableEntityIDs.Length == 2)
            {
                string entityID1 = linkableEntityIDs[0];
                string entityID2 = linkableEntityIDs[1];
                this._OrphanEntityIDs.Remove(entityID1);
                this._OrphanEntityIDs.Remove(entityID2);
                Dictionary<Entity, double> entityMembers=new Dictionary<Entity, double>();
                entityMembers.Add(this._Entities[entityID1], 1.0);
                entityMembers.Add(this._Entities[entityID2], 1.0);
                Cluster newCluster = new Cluster(this.NextClusterID(), metaData, entityMembers);
                this._Clusters.Add(newCluster.ClusterID,newCluster);
                this._RootClusterIDs.Add(newCluster.ClusterID);
                return true;
            }
            else
                return false;
        }

        private bool TryLinkClusterToCluster(EntityMetaData metaData)
        {
            double distance = double.PositiveInfinity;
            int[] linkableClusterIDs = new int[2];
            foreach(int clusterID1 in this._RootClusterIDs)
            {
                Cluster cluster1 = this._Clusters[clusterID1];
                foreach(int clusterID2 in this._RootClusterIDs)
                {
                    if(clusterID1!=clusterID2)
                    {
                        Cluster cluster2 = this._Clusters[clusterID2];
                        double cluster_cluster_distance = cluster1.CalculateDistanceToCluster(cluster2);
                        if(cluster_cluster_distance<distance)
                        {
                            distance = cluster_cluster_distance;
                            linkableClusterIDs[0] = clusterID1;
                            linkableClusterIDs[1] = clusterID2;
                        }
                    }
                }
            }

            if(distance<double.PositiveInfinity && linkableClusterIDs.Length==2)
            {
                int clusterID1 = linkableClusterIDs[0];
                this._RootClusterIDs.Remove(clusterID1);
                int clusterID2 = linkableClusterIDs[1];
                this._RootClusterIDs.Remove(clusterID2);
                Cluster cluster1 = this._Clusters[clusterID1];
                Dictionary<Entity, double> entityMembers1 = cluster1.EntityMembership;
                Cluster cluster2 = this._Clusters[clusterID2];
                Dictionary<Entity, double> entityMembers2 = cluster2.EntityMembership;
                Dictionary<Entity,double> mergedEntityMembers=new Dictionary<Entity, double>();
                double weight1 = ((double) entityMembers1.Count)/(entityMembers1.Count + entityMembers2.Count);
                double weight2 = ((double)entityMembers2.Count) / (entityMembers1.Count + entityMembers2.Count);
                foreach(Entity entity1 in entityMembers1.Keys)
                {
                    double membership = entityMembers1[entity1]*weight1;
                    if(entityMembers2.ContainsKey(entity1))
                        membership += entityMembers2[entity1]*weight2;
                    mergedEntityMembers.Add(entity1,membership);
                }
                foreach(Entity entity2 in entityMembers2.Keys)
                {
                    if(!mergedEntityMembers.ContainsKey(entity2))
                    {
                        mergedEntityMembers.Add(entity2,entityMembers2[entity2]*weight2);
                    }
                }
                Cluster newCluster=new Cluster(this.NextClusterID(),metaData,mergedEntityMembers);
                this._Clusters.Add(newCluster.ClusterID,newCluster);
                this._RootClusterIDs.Add(newCluster.ClusterID);
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool TryLinkEntityToCluster(EntityMetaData metaData)
        {
            double distance = double.PositiveInfinity;
            Entity entity = null;
            Cluster cluster = null;
            foreach (string entityID in this._OrphanEntityIDs)
            {
                entity = this._Entities[entityID];
                foreach(int clusterID in this._Clusters.Keys)
                {
                    cluster = this._Clusters[clusterID];
                    double entity_cluster_distance = cluster.CalculateDistanceToEntity(entity);
                    if(entity_cluster_distance <distance)
                    {
                        distance = entity_cluster_distance;
                    }
                }
            }

            if(distance<double.PositiveInfinity && entity !=null && cluster !=null)
            {
                this._OrphanEntityIDs.Remove(entity.Id);
                this._RootClusterIDs.Remove(cluster.ClusterID);
                Dictionary<Entity, double> entityMembers = cluster.EntityMembership;
                entityMembers.Add(entity,1.0);
                Cluster newCluster=new Cluster(this.NextClusterID(),metaData,entityMembers);
                this._Clusters.Add(newCluster.ClusterID,newCluster);
                this._RootClusterIDs.Add(newCluster.ClusterID);
                return true;
            }
            else
            {
                return false;
            }
        }

        private int NextClusterID()
        {
            int lastClusterID = 0;
            if(this._ClusteredEntities !=null && this._ClusteredEntities.Count>0)
            {
                foreach(Cluster cluster in this._ClusteredEntities.Keys)
                {
                    if(cluster.ClusterID>lastClusterID)
                        lastClusterID = cluster.ClusterID;
                }
            }
            return lastClusterID + 1;
        }
        #endregion
    }
}
